CDK Aspectsを使ってコンプライアンスに準拠したリソースチェックを自動化する

CDK Aspectsを使ってコンプライアンスに準拠したリソースチェックを自動化する

Clock Icon2024.12.28

CDK でインフラを構築、運用している際、以下のような課題に直面することはないでしょうか?

  • リソースの設定が組織のセキュリティポリシーに準拠しているか確認が大変
  • 複数のチームでの開発時にコンプライアンス要件の統一が難しい
  • インフラの規模が大きくなるにつれ、設定の見通しが悪くなってしまった

そんな時に CDK の Aspects を使うことで効率的に解決できるようなので、本記事では基本から入門し使い方を学んでいきます。

CDK Aspectsとは

CDK Aspects とは特定のスコープ内の Construct に対し、共通の操作が可能です。今回試してみる指定したリソースがルールに準拠しているかのチェックや、スタック全体にタグ付けするなどコンプライアンスを維持するために便利な機能です。

この仕組みを使って多くのルールを実装して提供されているcdk-nagというパッケージもあります。

CDK Aspectsを使ってリソースをチェックしてみる

以下の AWS ブログが分かりやすく解説されているため、こちらを参考に実装してみます。
https://aws.amazon.com/jp/blogs/news/align-with-best-practices-while-creating-infrastructure-using-cdk-aspects/

まずはスタックに VPC や SG などのリソースを追加します。

lib/cdk-aspect-stack.ts
import * as cdk from "aws-cdk-lib";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as rds from "aws-cdk-lib/aws-rds";
import * as s3 from "aws-cdk-lib/aws-s3";
import { Construct } from "constructs";

export class CdkAspectsStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    //Create a VPC with 3 availability zones
    const vpc = new ec2.Vpc(this, "MyVpc", {
      maxAzs: 3,
    });

    //Create a security group
    const sg = new ec2.SecurityGroup(this, "MySG", {
      vpc: vpc,
      allowAllOutbound: true,
    });

    //Add ingress rule for SSH from the public internet
    sg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(22), "SSH access from anywhere");

    //Launch an EC2 instance in private subnet
    const instance = new ec2.Instance(this, "MyInstance", {
      vpc: vpc,
      machineImage: ec2.MachineImage.latestAmazonLinux2(),
      instanceType: new ec2.InstanceType("t3.small"),
      vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
      securityGroup: sg,
    });

    //Launch MySQL rds database instance in private subnet
    const database = new rds.DatabaseInstance(this, "MyDatabase", {
      engine: rds.DatabaseInstanceEngine.mysql({
        version: rds.MysqlEngineVersion.VER_8_0,
      }),
      vpc: vpc,
      vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
      deletionProtection: false,
    });

    //Create an s3 bucket
    const bucket = new s3.Bucket(this, "MyBucket");
  }
}

ルールの追加

追加したリソースに対して、ベストプラクティスに沿った実装となるようにチェックするルールを追加します。

今回は以下6つのルールを追加していきます。

  • VPC の CIDR 範囲は特定の CIDR IP から始まる必要がある
  • セキュリティグループには public ingress ルールを設定してはいけない
  • EC2 インスタンスは許可されたインスタンスタイプのみが利用可能である
  • S3 バケットの暗号化を有効にする必要がある
  • S3 バケットのバージョン管理を有効にする必要がある
  • RDS インスタンスでは削除保護を有効にする必要がある
lib/cdk-aspect-rules.ts
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as rds from "aws-cdk-lib/aws-rds";
import * as s3 from "aws-cdk-lib/aws-s3";
import { Stack, IAspect, Annotations, Tokenization } from "aws-cdk-lib";
import { IConstruct } from "constructs";

//Verify VPC CIDR range
export class VPCCIDRAspect implements IAspect {
  public visit(node: IConstruct) {
    if (node instanceof ec2.CfnVPC && node.cidrBlock) {
      if (!node.cidrBlock.startsWith("192.168.")) {
        Annotations.of(node).addError(
          'VPC does not use standard CIDR range starting with "192.168."',
        );
      }
    }
  }
}

//Verify public ingress rule of security group
export class SecurityGroupNoPublicIngressAspect implements IAspect {
  public visit(node: IConstruct) {
    if (node instanceof ec2.CfnSecurityGroup) {
      checkRules(Stack.of(node).resolve(node.securityGroupIngress));
    }

    function checkRules(rules: Array<ec2.CfnSecurityGroup.IngressProperty>) {
      if (rules) {
        for (const rule of rules.values()) {
          if (
            !Tokenization.isResolvable(rule) &&
            (rule.cidrIp === "0.0.0.0/0" || rule.cidrIp === "::/0")
          ) {
            Annotations.of(node).addError("Security Group allows ingress from public internet.");
          }
        }
      }
    }
  }
}

//Verify instance type of EC2 instance
export class EC2ApprovedITAspect implements IAspect {
  public visit(node: IConstruct) {
    const its = ["x", "z", "p", "g", "i", "t"];
    if (node instanceof ec2.CfnInstance && node.instanceType) {
      const instanceType = node.instanceType;
      if (its.some((its) => instanceType.startsWith(its))) {
        Annotations.of(node).addError("EC2 Instance is not using approved instance type.");
      }
    }
  }
}

//Verify that bucket versioning is enabled
export class BucketVersioningAspect implements IAspect {
  public visit(node: IConstruct): void {
    if (node instanceof s3.CfnBucket) {
      if (
        !node.versioningConfiguration ||
        (!Tokenization.isResolvable(node.versioningConfiguration) &&
          node.versioningConfiguration.status !== "Enabled")
      ) {
        Annotations.of(node).addError("S3 bucket versioning is not enabled.");
      }
    }
  }
}

//Verify that bucket has server-side encryption enabled
export class BucketEncryptionAspect implements IAspect {
  public visit(node: IConstruct): void {
    if (node instanceof s3.CfnBucket) {
      if (!node.bucketEncryption) {
        Annotations.of(node).addError("S3 bucket encryption is not enabled.");
      }
    }
  }
}

//Verify that DB instance deletion protection is enabled
export class RDSDeletionProtectionAspect implements IAspect {
  public visit(node: IConstruct) {
    if (node instanceof rds.CfnDBInstance) {
      if (!node.deletionProtection) {
        Annotations.of(node).addError("RDS DB instance deletion protection is not enabled.");
      }
    }
  }
}

visit には全ての Construct が渡され、その中で対象とするリソースをnode instanceofで指定しています。
あとはルールとしたい内容に沿っていなかったらエラーを記述するといった流れでルールを書いていきます。

ルールの設定ができたので、これらをどのリソースに対してチェックするかを記述していきます。
特定のスコープ(App、Stack、Construct)に設定ができますが、今回は Stack のスコープに追加しています。

bin/cdk-aspect.ts
import * as cdk from "aws-cdk-lib";
import { Aspects } from "aws-cdk-lib";
import { CdkAspectsStack } from "../lib/cdk-aspect-stack";
import {
  BucketEncryptionAspect,
  BucketVersioningAspect,
  EC2ApprovedITAspect,
  RDSDeletionProtectionAspect,
  SecurityGroupNoPublicIngressAspect,
  VPCCIDRAspect,
} from "../lib/cdk-aspect-rules";

const app = new cdk.App();

const stack = new CdkAspectsStack(app, "MyApplicationStack");

Aspects.of(stack).add(new VPCCIDRAspect());
Aspects.of(stack).add(new SecurityGroupNoPublicIngressAspect());
Aspects.of(stack).add(new EC2ApprovedITAspect());
Aspects.of(stack).add(new RDSDeletionProtectionAspect());
Aspects.of(stack).add(new BucketEncryptionAspect());
Aspects.of(stack).add(new BucketVersioningAspect());

app.synth();

この状態でデプロイしてみると、以下のようにエラーが発生しデプロイできないようになりました。

❯ npx cdk deploy --all
[Error at /MyApplicationStack/MyVpc/Resource] VPC does not use standard CIDR range starting with "192.168."
[Error at /MyApplicationStack/MySG/Resource] Security Group allows ingress from public internet.
[Error at /MyApplicationStack/MyInstance/Resource] EC2 Instance is not using approved instance type.
[Error at /MyApplicationStack/MyDatabase/Resource] RDS DB instance deletion protection is not enabled.
[Error at /MyApplicationStack/MyBucket/Resource] S3 bucket encryption is not enabled.
[Error at /MyApplicationStack/MyBucket/Resource] S3 bucket versioning is not enabled.

Found errors

しっかりと各ルールに対してチェックが行われ、コンプライアンスに準拠した実装ができるようになりました。

ルールに沿って修正を行う

エラーメッセージに従って各リソースを修正します。

lib/cdk-aspect-stack.ts
import * as cdk from "aws-cdk-lib";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as rds from "aws-cdk-lib/aws-rds";
import * as s3 from "aws-cdk-lib/aws-s3";
import { Construct } from "constructs";

export class CdkAspectsStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    //Create a VPC with 3 availability zones
    const vpc = new ec2.Vpc(this, "MyVpc", {
      maxAzs: 3,
+      cidr: "192.168.0.0/16",
    });

    //Create a security group
    const sg = new ec2.SecurityGroup(this, "MySG", {
      vpc: vpc,
      allowAllOutbound: true,
    });

    //Add ingress rule for SSH from the public internet
    sg.addIngressRule(
+     ec2.Peer.ipv4("192.168.0.0/16"),
-     ec2.Peer.anyIpv4()
      ec2.Port.tcp(22),
      "SSH access from anywhere",
    );

    //Launch an EC2 instance in private subnet
    const instance = new ec2.Instance(this, "MyInstance", {
      vpc: vpc,
      machineImage: ec2.MachineImage.latestAmazonLinux2(),
+      instanceType: new ec2.InstanceType("m4.large"),
-      instanceType: new ec2.InstanceType("t3.large"),
      vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
      securityGroup: sg,
    });

    //Launch MySQL rds database instance in private subnet
    const database = new rds.DatabaseInstance(this, "MyDatabase", {
      engine: rds.DatabaseInstanceEngine.mysql({
        version: rds.MysqlEngineVersion.VER_8_0,
      }),
      vpc: vpc,
      vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
+      deletionProtection: true,
-      deletionProtection: true,
    });

    //Create an s3 bucket
    const bucket = new s3.Bucket(this, "MyBucket", {
+      encryption: s3.BucketEncryption.S3_MANAGED,
+      versioned: true,
    });
  }
}

修正が完了したので、再度デプロイしてみると問題なく完了しました。

 ✅  MyApplicationStack

✨  Deployment time: 535.42s

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:914661484899:stack/MyApplicationStack/275fe200-c4b3-11ef-90cf-0e90643eae69

✨  Total time: 537.59s

ルールに従って実装するガードレールが CDK でも作成できました。

まとめ

CDK Aspects を使ってリソースに対してルールのチェックを実装してみました。ルールをコード化しておくことで、どのようなルールが設定されているのか分かりやすいですね。

また、開発者は設定されているルールをデプロイ時に気づくことができるため、レビュー前に修正できることも心理的に楽になりそうです。

今回は AWS ブログに沿って実装しましたが、リソースの命名規則のチェックにも使えそうですし、他のリソースでも柔軟なチェックができそうです。

次は cdk-nag や CDK Aspects をテスト時にチェックするなど、より高度な部分を試していきたいですね。

参考

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.